/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.xml.handlers;

import java.net.URI;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.naming.OperationNotSupportedException;
import org.geotools.xml.DocumentFactory;
import org.geotools.xml.XMLElementHandler;
import org.geotools.xml.schema.All;
import org.geotools.xml.schema.Any;
import org.geotools.xml.schema.Choice;
import org.geotools.xml.schema.ComplexType;
import org.geotools.xml.schema.Element;
import org.geotools.xml.schema.ElementGrouping;
import org.geotools.xml.schema.ElementValue;
import org.geotools.xml.schema.Group;
import org.geotools.xml.schema.Sequence;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

/**
 * This class is intended to handle parsing an xml element from an instance document for elements
 * who's type is both known and complex. This handler is used within the XMLSAXHandler to handle sax
 * events generated by the SAX parser.
 *
 * @author dzwiers www.refractions.net
 * @see ComplexType
 */
public class ComplexElementHandler extends XMLElementHandler {

    /** <code>serialVersionUID</code> field */
    private static final long serialVersionUID = ComplexElementHandler.class.hashCode();

    private ComplexType type; // saves casting all over
    private Element elem;
    private String text;
    private Attributes attr;
    private List<XMLElementHandler> elements;
    private Object value = null;
    private ElementHandlerFactory ehf;

    /**
     * Creates a new ComplexElementHandler object for Element elem using ElementHandlerFactory ehf.
     *
     * @param ehf ElementHandlerFactory
     * @param elem Element
     */
    public ComplexElementHandler(ElementHandlerFactory ehf, Element elem) throws SAXException {
        this.ehf = ehf;

        if (elem == null) {
            logger.warning("ComplexType provided was null");
            throw new SAXException(new NullPointerException("ComplexType provided was null"));
        }

        this.elem = elem;

        try {
            type = (ComplexType) elem.getType();
        } catch (ClassCastException e) {
            logger.warning(e.toString());
            throw new SAXException(e);
        }
    }

    /** @see org.geotools.xml.XMLElementHandler#getElement() */
    @Override
    public Element getElement() {
        return elem;
    }

    /** @see org.geotools.xml.XMLElementHandler#characters(java.lang.String) */
    @Override
    public void characters(String text1) throws SAXException {
        if (type.isMixed()) {
            if (this.text != null) {
                this.text = this.text.concat(text1);
            } else {
                this.text = text1;
            }
        } else {
            if (!"".equals(text1.trim())) {
                if (type.getName() == null) {
                    throw new SAXException("This type may not have mixed content");
                }

                throw new SAXException(
                        "The " + type.getName() + " type may not have mixed content");
            }
        }
    }

    /** */
    @Override
    public void endElement(URI namespaceURI, String localName, Map<String, Object> hints)
            throws OperationNotSupportedException, SAXException {
        text = (text == null) ? null : text.trim();

        if (hints == null) {
            hints = new HashMap<>();
            hints.put(ElementHandlerFactory.KEY, ehf);
        } else {
            if (!hints.containsKey(ElementHandlerFactory.KEY))
                hints.put(ElementHandlerFactory.KEY, ehf);
        }

        if (elements == null) {
            if (type != null) {
                ElementValue[] vals;
                if (type.isMixed()) {
                    vals = new ElementValue[1];
                    vals[0] = new DefaultElementValue(null, text); // null is ok as
                    // this represents the mixed content
                } else {
                    vals = new ElementValue[0];
                }
                value = type.getValue(elem, vals, attr, hints);
            } else {
                value = text;
            }

            return;
        }

        // validate the complex element ... throws an exception when it's been bad
        boolean validate =
                hints == null
                        || !hints.containsKey(DocumentFactory.VALIDATION_HINT)
                        || hints.get(DocumentFactory.VALIDATION_HINT) == null
                        || !(hints.get(DocumentFactory.VALIDATION_HINT) instanceof Boolean)
                        || ((Boolean) hints.get(DocumentFactory.VALIDATION_HINT)).booleanValue();
        if (validate) validateElementOrder();

        ElementValue[] vals = new ElementValue[elements.size() + (type.isMixed() ? 1 : 0)];

        for (int i = 0; i < elements.size(); i++) {
            XMLElementHandler xeh = elements.get(i);
            vals[i] = new DefaultElementValue(xeh.getElement(), xeh.getValue());
        }

        if (type.isMixed()) {
            vals[vals.length - 1] = new DefaultElementValue(null, text);

            // null is ok as this represents the mixed content
        }

        value = type.getValue(elem, vals, attr, hints);

        // clean memory
        attr = null;
        elements = null;
        text = null;
    }

    /*
     * This starts off the fun or checking element order for complex types
     * (note mixtures of All, Any, Choice, Element, Group, Sequence).
     *
     */
    private void validateElementOrder() throws SAXException {
        if ((elements == null) || (elements.isEmpty())) {
            // TODO ensure we have enough elements
            return;
        }

        int i = 0;
        int count = 0;
        int[] i2 = new int[2];
        int cache = 0; // old pos.
        i2[1] = 1;
        while (i < elements.size() && i2[1] == 1) {
            i2[0] = i;
            i2[1] = 0;
            cache = i2[0];
            i2 = valid(type.getChild(), i);
            if (i2[1] == 0 && i == i2[0]) {
                // done running
                if (count < type.getChild().getMinOccurs()) {
                    StringBuffer buf = new StringBuffer();
                    buf.append("Too few elements for ");
                    buf.append(elem.getNamespace() + ":" + elem.getName());
                    buf.append(" (type = " + type.getName() + ") ");
                    buf.append(": ");
                    buf.append(count);
                    buf.append(" children, ");
                    buf.append(type.getChild().getMinOccurs());
                    buf.append(" minOccurs");
                    throw new SAXException(buf.toString());
                }
            } else {
                if (cache == i2[0]) {
                    // we have not progressed .. progress us
                    i = i2[0] + 1;
                } else {
                    i = i2[0];
                }
                count++;
            }
        }
        if (count > type.getChild().getMaxOccurs()) {
            StringBuffer buf = new StringBuffer();
            buf.append("Too many elements for ");
            buf.append(elem.getNamespace() + ":" + elem.getName());
            buf.append(" (type = " + type.getName() + ") ");
            buf.append(": ");
            buf.append(count);
            buf.append(" children, ");
            buf.append(type.getChild().getMaxOccurs());
            buf.append(" maxOccurs");
            throw new SAXException(buf.toString());
        }

        if (i != elements.size()) {
            StringBuffer buf = new StringBuffer();
            buf.append("Invalid Element ordering for ");
            buf.append(elem.getNamespace() + ":" + elem.getName());
            buf.append(" (type = " + type.getName() + ") ");
            buf.append(
                    ". There were "
                            + (elements.size() - i)
                            + "elements which were unaccounted for");
            throw new SAXException(buf.toString());
        }
    }

    /*
     * Generic validation method which simulates recursion, and avoids casting :)
     * The index is the starting index in the list of elements, for the particular
     * ElementGrouping. The last index matched is returned.
     */
    private int[] valid(ElementGrouping eg, int index) throws SAXException {
        if (eg == null) {
            return new int[] {index, 1};
        }

        switch (eg.getGrouping()) {
            case ElementGrouping.SEQUENCE:
                int[] tmp = valid((Sequence) eg, index);
                return tmp;

            case ElementGrouping.ALL:
                return valid((All) eg, index);

            case ElementGrouping.ANY:
                return valid((Any) eg, index);

            case ElementGrouping.CHOICE:
                return valid((Choice) eg, index);

            case ElementGrouping.GROUP:
                return valid((Group) eg, index);

            case ElementGrouping.ELEMENT:
                tmp = valid((Element) eg, index);
                return tmp;
        }

        return new int[] {index, 1};
    }

    /*
     * Validates an All tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(All all, int index) {
        Element[] elems = all.getElements();
        int[] r = new int[elems.length];

        for (int i = 0; i < r.length; i++) r[i] = 0;

        boolean c = true;
        int head = index;
        while (c) {
            c = false;

            for (int i = 0; i < elems.length; i++) {
                if (elems[i].getType().getName().equalsIgnoreCase(elements.get(head).getName())) {
                    r[i]++;
                    head++;
                    i = elems.length;
                    c = true;
                }
            }
        }

        for (int i = 0; i < r.length; i++) {
            if ((r[i] < elems[i].getMinOccurs()) || (r[i] > elems[i].getMaxOccurs())) {
                return new int[] {index, 0};
            }
        }

        return new int[] {head, 1};
    }

    /*
     * Validates an Any tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Any any, int index) {
        if (any.getNamespace().equals(elements.get(index).getElement().getType().getNamespace())) {
            return new int[] {index + 1, 1};
        }

        return new int[] {index, 1};
    }

    /*
     * Validates an Choice tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Choice choice, int index) throws SAXException {
        ElementGrouping[] eg = choice.getChildren();

        if (eg == null) {
            return new int[] {index, 1};
        }

        int i = 0; // choice child index;

        int end = index;
        int t = index;
        int count = 0;
        int[] t2 = null;
        while (i < eg.length && end < elements.size()) {
            t2 = valid(eg[i], t);
            if (t2[1] == 0 && t2[0] == t) { // nothing, next
                // move along
                if (t2[0] > end && count >= eg[i].getMinOccurs() && count <= eg[i].getMaxOccurs())
                    end = t2[0];
                count = 0;
                i++;
                t = index;
            } else {
                if (count == eg[i].getMaxOccurs()) {
                    // move along
                    if (t2[0] > end && count >= eg[i].getMinOccurs()) end = t2[0];
                    count = 0;
                    i++;
                    t = index;
                } else {
                    t = t2[0];
                    if (t == elements.size()) {
                        end = t;
                    }
                    count++;
                }
            }
        }

        return new int[] {end, end == index ? 0 : 1};
    }

    /*
     * Validates an Group tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Group group, int index) throws SAXException {
        if (group.getChild() == null) {
            return new int[] {index, 1};
        }

        return valid(group.getChild(), index);
    }

    /*
     * Validates an Element tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Element element, int index) {

        // does this element equate to the index in the doc?

        int[] r = null;

        XMLElementHandler indexHandler = null;
        if (index < elements.size()) {
            indexHandler = elements.get(index);
        } else {
            // not found :)
            return new int[] {index, 0};
        }

        if (r == null && (indexHandler == null || indexHandler.getElement() == null))
            return new int[] {index, 0};

        if (r == null && indexHandler.getElement() == element) r = new int[] {index + 1, 1};

        if (r == null && element.getName() == null) return new int[] {index, 0};

        if (r == null
                && (element.getName() != null
                        && element.getName().equalsIgnoreCase(indexHandler.getName())))
            r = new int[] {index + 1, 1};
        if (r == null && element.getName() != null) {
            Element e = indexHandler.getElement();
            while (r == null && e != null) {
                if (element.getName().equalsIgnoreCase(e.getName())) {
                    r = new int[] {index + 1, 1};
                }
                e = e.getSubstitutionGroup();
            }
        }

        if (r == null) {
            r = new int[] {index, 0};
        }
        return r;
    }

    /*
     * Validates a Sequence tag
     * @see valid(ElementGrouping)
     */
    private int[] valid(Sequence seq, int index) throws SAXException {
        ElementGrouping[] eg = seq.getChildren();

        if (eg == null) {
            return new int[] {index, 1};
        }

        int tIndex = index; // top of element matching list
        int t = 0; // top of child list

        int count = 0; // used for n-ary at a single spot
        int[] i2 = new int[2];
        while (t < eg.length && tIndex < elements.size()) {
            i2 = valid(eg[t], tIndex); // new top element
            if (i2[1] == 1) { // they matched
                if (tIndex == i2[0]) {
                    // didn't more ahead ...
                    t++; // force next spot
                    count = 0; // reset
                } else {
                    count++;
                    if (count <= eg[t].getMaxOccurs()) {
                        tIndex = i2[0]; // store index
                    } else {
                        // error, so redo
                        if (eg[t].getMinOccurs() > count) {
                            // not good
                            // System.out.println("Seq Failed");
                            return new int[] {index, 0}; // not whole sequence
                        }
                        t++;
                        count = 0; // next defined type
                    }
                }
            } else {
                // didn't match

                // move along and retest that spot
                if (eg[t].getMinOccurs() > count) {
                    // not good
                    // System.out.println("Seq Failed");
                    return new int[] {index, 0}; // not whole sequence
                }
                t++;
                count = 0; // next defined type
            }
        }
        // System.out.println("Seq index = "+tIndex+" Matched");
        return new int[] {tIndex, 1};
    }

    /**
     * TODO summary sentence for startElement ...
     *
     * @see org.geotools.xml.XMLElementHandler#startElement(java.net.URI, java.lang.String,
     *     org.xml.sax.Attributes)
     */
    @Override
    public void startElement(URI namespaceURI, String localName, Attributes attr1) {
        this.attr = new AttributesImpl(attr1);
    }

    /**
     * TODO summary sentence for getHandler ...
     *
     * @see org.geotools.xml.XMLElementHandler#getHandler(java.net.URI, java.lang.String,
     *     java.util.Map)
     * @return XMLElementHandler
     */
    @Override
    public XMLElementHandler getHandler(
            URI namespaceURI, String localName, Map<String, Object> hints) throws SAXException {
        if (elements == null) {
            elements = new LinkedList<>();
        }

        logger.finest("Starting search for element handler " + localName + " :: " + namespaceURI);

        Element e = XMLTypeHelper.findChildElement(type, localName, namespaceURI);
        if (e != null && namespaceURI.equals(e.getNamespace())) {
            XMLElementHandler r = ehf.createElementHandler(e);

            if (type.cache(r.getElement(), hints)) {
                elements.add(r);
            }

            return r;
        }

        logger.finest("Checking the document schemas");

        XMLElementHandler r = ehf.createElementHandler(namespaceURI, localName);

        if (r != null) {
            if (type.cache(r.getElement(), hints)) {
                elements.add(r);
            }

            return r;
        }

        // validation?
        if (hints != null && hints.containsKey(DocumentFactory.VALIDATION_HINT)) {
            Boolean valid = (Boolean) hints.get(DocumentFactory.VALIDATION_HINT);
            if (valid != null && !valid.booleanValue()) {
                return new IgnoreHandler();
            }
        }

        throw new SAXException(
                "Could not find element handler for "
                        + namespaceURI
                        + " : "
                        + localName
                        + " as a child of "
                        + type.getName()
                        + ".");
    }

    /**
     * TODO summary sentence for getValue ...
     *
     * @see org.geotools.xml.XMLElementHandler#getValue()
     * @return Object
     */
    @Override
    public Object getValue() {
        // endElement sets the value
        return value;
    }

    /** @see org.geotools.xml.XMLElementHandler#getName() */
    @Override
    public String getName() {
        return elem.getName();
    }

    /** Remove the given XMLElementHandler from the Child-List */
    public void removeElement(XMLElementHandler handler) {
        if (elements != null) {
            elements.remove(handler);
        }
    }

    /**
     * returns the Type of the Elementhandler
     *
     * @return type
     */
    public ComplexType getType() {
        return type;
    }

    /**
     * Default Implementation used to pass values to type instances
     *
     * @author dzwiers
     * @see ElementValue
     */
    private static class DefaultElementValue implements ElementValue {
        Element t;
        Object value;

        /**
         * Stores the two values for use within the specified type
         *
         * @param t Element
         * @param o String
         */
        public DefaultElementValue(Element t, Object o) {
            this.t = t;
            value = o;
        }

        /**
         * TODO summary sentence for getElement ...
         *
         * @see org.geotools.xml.schema.ElementValue#getElement()
         * @return Element
         */
        @Override
        public Element getElement() {
            return t;
        }

        /**
         * TODO summary sentence for getValue ...
         *
         * @see org.geotools.xml.schema.ElementValue#getValue()
         * @return Object
         */
        @Override
        public Object getValue() {
            return value;
        }
        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer();
            if (t != null && t.toString() != null) {
                buf.append(t.toString());
            } else {
                buf.append(getClass().getName());
            }
            buf.append("[");
            buf.append(value);
            buf.append("]");
            return buf.toString();
        }
    }
}
